package com.rollway.restful;

import com.rollway.restful.annotation.EnableRestfulClients;
import com.rollway.restful.annotation.RestfulClient;
import com.rollway.restful.annotation.SingleOptions;
import com.rollway.restful.options.CompositeRestfulOptions;
import com.rollway.restful.options.RestfulOptions;
import com.rollway.restful.support.MethodMetadataParser;
import com.rollway.restful.utils.Util;
import okhttp3.OkHttpClient;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import sun.reflect.annotation.AnnotationType;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;


public class RestfulClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware, BeanFactoryAware {
    private ResourceLoader resourceLoader;
    private Environment environment;
    private BeanFactory beanFactory;
    private CompositeRestfulOptions compositeRestfulOptions;
    private OkHttpClient okHttpClient;
    private MethodMetadataParser methodMetadataParser;
    private List<HttpMessageConverter<?>> httpMessageConverters;
    private List<RequestCustomizer> requestCustomizers = new ArrayList<>();
    private boolean initialized = false;

    RestfulClientsRegistrar() {
    }


    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        registerRestFulClients(metadata, registry);
    }

    public void registerRestFulClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(RestfulClient.class));
        Set<String> basePackages = getBasePackages(metadata);
        for (String basePackage : basePackages) {
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(), "@RestfulClient can only be specified on an interface");
                Map<String, Object> attributes = annotationMetadata
                        .getAnnotationAttributes(RestfulClient.class.getCanonicalName());
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }

    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
                                     Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        Class clazz = ClassUtils.resolveClassName(className, null);
        RestfulClientFactoryBean factoryBean = new RestfulClientFactoryBean();
        RestfulOptions restFulOptions = factoryBean.getRestfulOptions();
        factoryBean.setType(clazz);
        String name = getName(environment, attributes, className);
        restFulOptions.setServiceName(name);
        restFulOptions.setScheme((String) Util.resolve(environment, attributes.get("scheme")));
        restFulOptions.setAuthority((String) Util.resolve(environment, attributes.get("authority")));
        restFulOptions.setPrefix((String) Util.resolve(environment, attributes.get("prefix")));
        restFulOptions.setMethodKey(Util.DEFAULT);
        Set<String> keySet = AnnotationType.getInstance(SingleOptions.class).members().keySet();
        attributes.entrySet().stream().filter(e -> keySet.contains(e.getKey()))
                .filter(e -> Util.isValid(e.getValue()))
                .forEach(entry -> {
                    RestfulOptions.Options options = restFulOptions.getOrInitOptions();
                    Util.fillFields(environment, options, entry);
                });
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(clazz, () -> {
                    initDependency();
                    initFactoryBean(factoryBean);
                    return factoryBean.getObject();
                });
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className);
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

    private void initDependency() {
        if (!initialized) {
            this.methodMetadataParser = beanFactory.getBean(MethodMetadataParser.class);
            this.okHttpClient = beanFactory.getBean(OkHttpClient.class);
            this.compositeRestfulOptions = beanFactory.getBean(CompositeRestfulOptions.class);
            if (beanFactory instanceof DefaultListableBeanFactory) {
                DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) this.beanFactory;
                Collection<RequestCustomizer> values = listableBeanFactory.getBeansOfType(RequestCustomizer.class)
                        .values();
                requestCustomizers.addAll(values);
            }
            this.httpMessageConverters = getSimpleHttpMessageConverters();
        }
    }

    private List<HttpMessageConverter<?>> getSimpleHttpMessageConverters() {
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.getMessageConverters();
    }

    private void initFactoryBean(RestfulClientFactoryBean factoryBean) {
        factoryBean.setOkHttpClient(okHttpClient);
        factoryBean.setMethodMetadataParser(methodMetadataParser);
        factoryBean.setRestFulOptionsComposite(compositeRestfulOptions);
        factoryBean.setHttpMessageConverters(httpMessageConverters);
        factoryBean.setRequestCustomizers(requestCustomizers);
    }

    String getName(Environment environment, Map<String, Object> attributes, String className) {
        String name = (String) Util.resolve(environment, attributes.get("value"));
        return StringUtils.hasText(name) ? name : className;
    }


    protected ClassPathScanningCandidateComponentProvider getScanner() {
        return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                boolean isCandidate = false;
                if (beanDefinition.getMetadata().isIndependent()) {
                    if (!beanDefinition.getMetadata().isAnnotation()) {
                        isCandidate = true;
                    }
                }
                return isCandidate;
            }
        };
    }

    protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> attributes = importingClassMetadata
                .getAnnotationAttributes(EnableRestfulClients.class.getCanonicalName());
        Set<String> basePackages = new HashSet<>();
        for (String pkg : (String[]) attributes.get("value")) {
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg);
            }
        }
        for (String pkg : (String[]) attributes.get("basePackages")) {
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg);
            }
        }
        if (basePackages.isEmpty()) {
            basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));
        }
        return basePackages;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }


    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
}
